/**
 * Licensed under the MIT License
 * 
 * Copyright (c) 2010 Ali Nakipoglu
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 * */

package com.alinakipoglu.utils.ascheduler
{
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.events.TimerEvent;
	import flash.utils.Dictionary;
	import flash.utils.Timer;
	import flash.utils.getTimer;
	
	/**
	 * Dispatched when all functions removed (completed).
	 */
	[Event(name="complete", type="flash.events.Event")]
	
	/**
	 * AScheduler Class is an attempt to schedule function executions by assigning
	 * time (Milliseconds) range per function. Multiple instance can be created to group
	 * functions. Due to single threaded design of Actionscript it's very difficult to
	 * find elegant solution for threading (for me at least).
	 * 
	 * This is highly experimental work!
	 * 
	 * More info and original study can be found at :
	 * 
	 * http://www.senocular.com/flash/tutorials/asyncoperations/ 
	 * 
	 * Even More info :
	 * 
	 * First in first out
	 * 
	 * @author 	ALI NAKIPOGLU
	 * @version	0.01c
	 * */
	public class AScheduler extends EventDispatcher
	{
		private var m_functions						:Vector.<Function>;
		
		private var m_fixedMSPerFunction			:Dictionary;
		private var m_argumentsPerFuncton			:Dictionary;
		private var m_userDataPerFunction			:Dictionary;
		private var m_startTimePerFunction			:Dictionary;
		
		private var m_maxMS							:uint;
		private var m_reservedMS					:uint;
		private var m_availableMS					:uint;
		private var m_totalMS						:uint;
		private var m_updateRate					:uint;
		private var m_removedFunctionDuringUpdate	:uint;
		
		private var m_updating						:Boolean;
		
		private var m_timer							:Timer;
		
		/**
		 * AScheduler constructor method.
		 * 
		 * @param _maxMS Maximum available time (Milliseconds) for this instance.
		 * @param _reservedMS Time reserved for Flash Player. 
		 * 
		 */
		public function AScheduler(_maxMS:uint, _reservedMS:uint)
		{
			m_functions						= new Vector.<Function>();
			
			m_fixedMSPerFunction			= new Dictionary();
			m_argumentsPerFuncton			= new Dictionary();
			m_userDataPerFunction			= new Dictionary();
			m_startTimePerFunction			= new Dictionary();
			
			m_maxMS							= _maxMS;
			m_reservedMS					= _reservedMS;
			m_availableMS					= 0;
			m_totalMS						= 0;
			m_updateRate					= uint.MAX_VALUE;
			m_removedFunctionDuringUpdate	= 0;
			
			m_updating						= false;
			
			m_timer							= new Timer(1000); // This interval value replaced in start method.
			
			m_timer.addEventListener(TimerEvent.TIMER, handleTimerUpdateEvent);
			
			updateAvailableMS();
		}
		
		/**
		 * 	Best update interval calculated.
		 */
		public function get updateRate():uint
		{
			return m_updateRate;
		}

		/**
		 * Maximum available time been set.
		 */
		public function get maxMS():uint
		{
			return m_maxMS;
		}
		
		/**
		 * @private
		 */
		public function set maxMS(_ms:uint):void
		{
			m_maxMS			= _ms;
			
			updateAvailableMS();
		}
		
		/**
		 * Reserved time.
		 */
		public function get reservedMS():uint
		{
			return m_reservedMS;
		} 
		
		/**
		 * @private
		 */
		public function set reservedMS(_ms:uint):void
		{
			m_reservedMS	= _ms;
			
			updateAvailableMS();
		}
		
		/**
		 * Current total available time. 
		 */
		public function get availableMS():uint
		{
			return m_availableMS;
		}
		
		/**
		 * Returns available time by ratio between 0-1.
		 * 
		 * @param _ratio Ratio between 0-1.
		 * @return uint avaliable time value.
		 */
		public function getAvailableMSByRatio(_ratio:Number):uint
		{
			return Math.floor(m_availableMS * _ratio);
		}
	
		/**
		 * Adds function that may executed if available time found.
		 *  
		 * @param _function Function to be executed.
		 * @param _ms Reserved time for this function per iteration. 
		 * @param _arguments Arguments applied every time function executed.
		 * @param _userData An initial user data can be assigned.
		 */
		public function addFunction(_function:Function, _ms:uint, _arguments:Array = null, _userData:* = null):void
		{
			m_functions[m_functions.length]			=_function;
				
			m_fixedMSPerFunction[_function]			= _ms;
			m_argumentsPerFuncton[_function]		= _arguments;
			m_userDataPerFunction[_function]		= _userData;
			
			m_totalMS								+= _ms;
			
			if(m_updateRate > _ms + m_reservedMS)
			{
				m_updateRate						= _ms + m_reservedMS;
			}
			
			updateAvailableMS();
		}
		
		/**
		 * Removes specified function.
		 * @param _function Function to removed.
		 */
		public function removeFunction(_function:Function):void
		{
			var _functionIndex	:uint				= m_functions.indexOf(_function);
			
			if(_functionIndex != -1)
			{
				m_fixedMSPerFunction[_function]		= null;
				m_argumentsPerFuncton[_function]	= null;
				m_userDataPerFunction[_function]	= null;
				
				m_functions.splice(_functionIndex, 1);
				
				if(m_updating)
				{
					m_removedFunctionDuringUpdate++;
				}
				
				if(m_functions.length == 0)
				{
					stop();
					dispatchEvent(new Event(Event.COMPLETE));	
				}
			}
		}
		
		/**
		 * Handy method to check function can continue to execute or not.
		 * 
		 * @param _function Function to check.
		 * @return Boolean value.
		 */
		public function getCanFunctionContinue(_function:Function):Boolean
		{
			if(m_functions.indexOf(_function) != -1)
			{
				if(getTimer() - m_startTimePerFunction[_function] < m_fixedMSPerFunction[_function])
				{
					return true;	
				}
			}
			
			return false;
		}
		
		/**
		 * Returns specified function's user data that been set by addFunction method or setFunctionUserData.
		 * @param _function Function.
		 * @return User data.
		 */
		public function getFunctionUserData(_function:Function):*
		{
			return m_userDataPerFunction[_function];
		}
		
		/**
		 * Returns specified function's arguments that been set by addFunction method or setFunctionArguments.
		 * @param _function Function.
		 * @return Arguments.
		 */
		public function getFunctionArguments(_function:Function):Array
		{
			return m_argumentsPerFuncton[_function];
		}
		
		/**
		 * Updates function's user data.
		 * @param _function Function to update User Data.
		 * @param _userData User Data.
		 */
		public function setFunctionUserData(_function:Function, _userData:*):void
		{
			m_userDataPerFunction[_function]		= _userData;
		}
		
		/**
		 * Updated specified functions arguments.
		 * @param _function Function to update arguments.
		 * @param _arguments Arguments array.
		 */
		public function setFunctionArguments(_function:Function, _arguments:Array):void
		{
			m_argumentsPerFuncton[_function]		= _arguments;
		}
		
		/**
		 * Starts updating functions. 
		 */
		public function start():void
		{
			if(m_functions.length > 0)
			{
				m_timer.delay			= m_updateRate;
				
				m_timer.start();
			}
		}
		
		/**
		 * Stops updating functions. 
		 */
		public function stop():void
		{
			m_timer.stop();
		}
		
		private function update():void
		{
			m_updating					= true;
			
			var _functionCount	:uint	= m_functions.length;
			var _index			:uint	= 0;
			var _startTime		:uint	= 0;
			var _cumulativeTime	:uint	= 0;
			
			var _updateTime		:uint	= m_maxMS - m_reservedMS;
			
			var _function		:Function;
			var _arguments		:Array;
			
			while(	_functionCount - m_removedFunctionDuringUpdate > 0 &&
					_cumulativeTime + m_fixedMSPerFunction[m_functions[_index]] < _updateTime)
			{
				_startTime				= getTimer();
				
				_function				= m_functions[_index];
				_arguments				= m_argumentsPerFuncton[_function];
				
				m_startTimePerFunction[_function]	= _startTime
					
				if(_arguments)
				{
					_function.apply(null, _arguments);
				} else {
					_function();
				}
				
				_cumulativeTime	+= getTimer() - _startTime;
				
				_index++;
				
				if(_index == _functionCount)
				{
					_index	= 0;
				}
			}
			m_removedFunctionDuringUpdate	= 0;
			m_updating						= false;
		}
		
		private function updateAvailableMS():void
		{
			m_availableMS	= m_maxMS - (m_reservedMS + m_totalMS);
		}
		
		private function handleTimerUpdateEvent(event:Event):void
		{
			update();
		}
	}
}